#light
#r @"..\..\source\WebTools.Core\bin\webtools.core.dll"

open System
open System.IO
open System.Text
open System.Reflection

open FSharp.WebTools

// ------------------------------------------------------------------------------------------------
// Utilities

let dispose f = 
  { new IDisposable with Dispose() = f () }

let color c =
  let d = dispose(let clr = Console.ForegroundColor in fun () -> Console.ForegroundColor <- clr)
  Console.ForegroundColor <- c
  d

type StatCounter() = 
  let mutable count = 0
  let mutable classes = 0
  member x.ReportError fmt = 
    use d = color ConsoleColor.Red
    print_string fmt    
    count <- count + 1    
  member x.Count = count
  member x.Classes with get() = classes and set(v) = classes <- v
  
let stat = StatCounter()  

let createResponse () =  
  let sb = new StringBuilder() 
  { new IResponse with 
      member x.Write s = sb.Append(s) |> ignore
      member x.End() = ()
      member x.ContentType with set(v) = () }, fun () -> sb.ToString()

let testJavaScript (fn, str:string) = 
  if (str.IndexOf("JS Translation: Not supported operation") <> -1) then
    stat.ReportError(sprintf "Error: JS translator found not supported operation in '%s'\n  %s"
                            fn "This means that some Expr representation has probably changed.")
                            
  let mutable last = str.IndexOf("registerClass") + 1
  while last <> 0 do
    stat.Classes <- stat.Classes + 1
    last <- str.IndexOf("registerClass", last) + 1
      
// ------------------------------------------------------------------------------------------------
// Generate JavaScript for specified client-side page and verify the result
// (we need two instances for generating two diferent JS files)
      
let checkPage (pg1:ClientPage, pg2:ClientPage) = 
  let fn = pg1.GetType().Name
  try
    let fn = fn.ToLower()
    let resp, reader = createResponse()
    pg1.GenerateCoreScriptFile(resp)
    let strCore = reader()
    let resp, reader = createResponse()
    pg2.GeneratePageScript(true, resp)
    let strPage = reader()
      
    [ fn+"_core", strCore; fn+"_page", strPage ] |> List.iter testJavaScript 
    
    File.WriteAllText(sprintf "output\\%s_core.js" fn, strCore)
    File.WriteAllText(sprintf "output\\%s_page.js" fn, strPage)
    
    printfn "  Generated JS length - core (%d) page (%d)" strCore.Length strPage.Length
  with e ->
    stat.ReportError(sprintf "Error: Generating JS code for '%s' failed.\n\n%A\n\n" fn e)  

// ------------------------------------------------------------------------------------------------
// Analyze assemblies in the 'bin' directory and find compiled client-side 
// pages and other client-side clesses 
  
let clientPageType = typeof<ClientPage>
let getAssemblyTypes dir file = 
  try
    let asm = Assembly.LoadFile(file)
    let ty = asm.GetTypes()
    let clientPages, otherClasses = 
      [ for p in asm.GetTypes() do
          if clientPageType.IsAssignableFrom(p) &&  // We need only ClientPage pages, but only the implementation from ".aspx.fs"
             not (p.Name.EndsWith("_aspx")) then    // file, so the second condition ignores geenrated ASP.NET classes
            yield (true, p)
          elif (p.GetCustomAttributes(typeof<ClientSideAttribute>, false).Length <> 0 ||
                p.GetCustomAttributes(typeof<NeutralSideAttribute>, false).Length <> 0 ||
                p.GetCustomAttributes(typeof<MixedSideAttribute>, false).Length <> 0) then
            yield (false, p) ]
      |> List.partition (fun (b, ty) -> b)
    List.map snd clientPages, List.map snd otherClasses
  with e ->
    stat.ReportError(sprintf "Error: Unexpected failure while processing:\n  '%s'\n\n%s\n\n" file (e.ToString()))
    [], []

// ------------------------------------------------------------------------------------------------
// Create instances of client-side pages, so we can invoke JS translator on them
// Other client-side types that were found are registered with the first page 
// (so the page generates JS code for them as well)

let verifyPagesAndClasses (clientPages:Type list, otherClasses:Type list) = 
  let pageInstances = 
    [| for p in clientPages do
         match 
          (try 
             let ct = p.GetConstructor([| |]) in 
             Some(p, ((ct.Invoke([| |]) :?> ClientPage), (ct.Invoke([| |]) :?> ClientPage)))
           with e ->
             do stat.ReportError(sprintf "Error: Instantiation of '%s' failed.\n\n%s\n\n" p.Name (e.ToString()) )
             None) with 
         | Some(t) -> yield t
         | _ -> do () |]

  if pageInstances.Length = 0 && otherClasses.Length > 0 then
    stat.ReportError("Error: No page found, but other client-side classes exist.\n  This seems like a test project issue, the classes will be skipped.\n")
  else
    use d = color ConsoleColor.DarkGray
    let pageForCtls = (snd >> snd) (pageInstances.[0])
    for c in otherClasses do
      printfn "  Client-side type: %s" c.FullName
      try
        pageForCtls.RegisterClientClass c
      with e ->
        stat.ReportError(sprintf "Error: Generating JS code for '%s' failed.\n\n%A\n\n" c.Name e)
    for p, pg in pageInstances do   
      printfn "  Client-side page: %s" p.FullName
      checkPage pg

// ------------------------------------------------------------------------------------------------
// Walk through all the directories with demos
  
let processDirectories () =
  for d in Directory.GetDirectories(Sys.argv.[1]) do
    printfn "\nCHECKING %s" d
    let bin = Path.GetFullPath(Path.Combine(d, "bin"))
    if not (Directory.Exists(bin)) then
      stat.ReportError("Error: Bin directory does not exist. Did the application compile correctly?\n")
    else
    
      let handler = new ResolveEventHandler(fun sender eargs ->
        let fn = Path.Combine(bin, eargs.Name.Substring(0, eargs.Name.IndexOf(',')) + ".dll")
        if File.Exists(fn) then
          Assembly.LoadFile(fn)
        else
          failwithf "Assembly not found '%s'!" fn )
      AppDomain.CurrentDomain.add_AssemblyResolve(handler)
      use h = dispose(fun () -> AppDomain.CurrentDomain.remove_AssemblyResolve(handler) )

      let files = Directory.GetFiles(bin, "*.dll")
      let clientPages, otherClasses as res = 
        files |> Array.fold_left (fun (lPg, lCl) file ->
          let clientPages, otherClasses = getAssemblyTypes bin file
          (lPg @ clientPages, lCl @ otherClasses) ) ([], [])
      verifyPagesAndClasses res
      
do
  if (Sys.argv.Length < 2) then
    print_string "Usage:\n  tester.exe [AppsDirectory]\n\nAppsDirectory - Should contain pre-compiled ASP.NET projects"
  else  
    Directory.CreateDirectory("output") |> ignore
    processDirectories ()
    use c = color ConsoleColor.Green
    printf "\nSUMMARY Errors: %d, Classes: %d" stat.Count stat.Classes
  